name: CI

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

env:
  CARGO_INCREMENTAL: 0
  CARGO_NET_RETRY: 10
  CARGO_TERM_COLOR: always
  RUSTUP_MAX_RETRIES: 10
  PYTHON_VERSION: "3.12"

jobs:
  cargo-fmt:
    name: "cargo fmt"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install Rustfmt"
        run: rustup component add rustfmt

      - name: "rustfmt"
        run: cargo fmt --all --check

      - name: "Prettier"
        run: npx prettier --check "**/*.{json5,yaml,yml}"

      - name: "Ruff format"
        run: pipx run ruff format --diff .

      - name: "Ruff check"
        run: pipx run ruff check .

  cargo-clippy:
    strategy:
      matrix:
        include:
          - os: "ubuntu"
            runner: "ubuntu-latest"
          - os: "windows"
            runner: "windows-latest-large"
      fail-fast: false
    runs-on: ["${{ matrix.runner }}"]
    name: "cargo clippy | ${{ matrix.os }}"
    steps:
      - uses: actions/checkout@v4
      - name: "Install Rust toolchain"
        run: rustup component add clippy
      - uses: Swatinem/rust-cache@v2
        with:
          save-if: ${{ github.ref == 'refs/heads/main' }}
      - name: "Clippy"
        run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings

  cargo-test-unix:
    strategy:
      matrix:
        include:
          # We use the large GitHub actions runners
          # For Ubuntu and Windows, this requires Organization-level configuration
          # See: https://docs.github.com/en/actions/using-github-hosted-runners/about-larger-runners/about-larger-runners#about-ubuntu-and-windows-larger-runners
          - os: "ubuntu"
            runner: "ubuntu-latest-large"
          - os: "macos"
            runner: "macos-14"
      fail-fast: false
    runs-on:
      labels: ${{ matrix.runner }}
    name: "cargo test | ${{ matrix.os }}"
    steps:
      - uses: actions/checkout@v4
      - name: "Install Rust toolchain"
        run: rustup show

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2

      - name: "Install required Python versions"
        run: |
          cargo run -p uv-dev -- fetch-python

      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest

      - name: "Cargo test"
        run: |
          cargo nextest run \
            --workspace \
            --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow

      - name: "Smoke test"
        run: |
          uv="./target/debug/uv"
          $uv venv
          $uv pip install ruff

  cargo-test-windows:
    strategy:
      matrix:
        include:
          - os: "windows"
            runner: "windows-latest-large"
      fail-fast: false
    runs-on:
      labels: ${{ matrix.runner }}
    name: "cargo test | ${{ matrix.os }}"
    steps:
      - uses: actions/checkout@v4
      - name: "Install Rust toolchain"
        run: rustup show

      # We do not test with Python patch versions on Windows
      # so we can use `setup-python` instead of our bootstrapping code
      # this is much faster on the extremely slow GitHub Windows runners.
      - uses: actions/setup-python@v5
        with:
          python-version: |
            3.8
            3.9
            3.10
            3.11
            3.12

      - uses: Swatinem/rust-cache@v2

      - name: "Install cargo nextest"
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-nextest

      - name: "Cargo test"
        run: |
          cargo nextest run --no-default-features --features python,pypi,git --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow

      - name: "Smoke test"
        run: |
          Set-Alias -Name uv -Value ./target/debug/uv
          uv venv
          uv pip install ruff

  # Separate job for the nightly crate
  windows-trampoline:
    runs-on: windows-latest
    name: "check windows trampoline"
    steps:
      - uses: actions/checkout@v4
      - name: "Install Rust toolchain"
        working-directory: crates/uv-trampoline
        run: |
          rustup target add x86_64-pc-windows-msvc
          rustup component add clippy rust-src --toolchain nightly-2024-03-19-x86_64-pc-windows-msvc
      - uses: rui314/setup-mold@v1
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: "crates/uv-trampoline"
      - name: "Clippy"
        working-directory: crates/uv-trampoline
        run: cargo clippy --all-features --locked --target x86_64-pc-windows-msvc -- -D warnings
      - name: "Build"
        working-directory: crates/uv-trampoline
        run: cargo build --release --target x86_64-pc-windows-msvc

  build-binary-linux:
    runs-on:
      labels: ubuntu-latest-large
    name: "build binary | linux"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - name: "Setup musl"
        run: |
          sudo apt-get install musl-tools
          rustup target add x86_64-unknown-linux-musl

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build --target x86_64-unknown-linux-musl

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}
          path: ./target/x86_64-unknown-linux-musl/debug/uv
          retention-days: 1

  build-binary-macos-aarch64:
    runs-on:
      labels: macos-14
    name: "build binary | macos aarch64"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}
          path: ./target/debug/uv
          retention-days: 1

  build-binary-macos-x86_64:
    runs-on:
      labels: macos-latest-large
    name: "build binary | macos x86_64"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-macos-x86_64-${{ github.sha }}
          path: ./target/debug/uv
          retention-days: 1

  build-binary-windows:
    runs-on:
      labels: windows-latest-large
    name: "build binary | windows"
    steps:
      - uses: actions/checkout@v4

      - uses: rui314/setup-mold@v1

      - uses: Swatinem/rust-cache@v2
      - name: "Build"
        run: cargo build

      - name: "Upload binary"
        uses: actions/upload-artifact@v4
        with:
          name: uv-windows-${{ github.sha }}
          path: ./target/debug/uv.exe
          retention-days: 1

  ecosystem-test:
    needs: build-binary-linux
    name: "ecosystem test | ${{ matrix.repo }}"
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - repo: "prefecthq/prefect"
            command: "uv pip install -e '.[dev]'"
            python: "3.9"
          - repo: "pallets/flask"
            command: "uv pip install -r requirements/dev.txt"
            python: "3.12"
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
        with:
          repository: ${{ matrix.repo }}

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Test"
        run: |
          ./uv venv
          ./${{ matrix.command }}

  cache-test-ubuntu:
    needs: build-binary-linux
    name: "check cache | ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Download binary for last version"
        run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-gnu.tar.gz" | tar -xvz

      - name: "Check cache compatibility"
        run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-x86_64-unknown-linux-gnu/uv

  cache-test-macos-aarch64:
    needs: build-binary-macos-aarch64
    name: "check cache | macos aarch64"
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Download binary for last version"
        run: curl -LsSf "https://github.com/astral-sh/uv/releases/latest/download/uv-aarch64-apple-darwin.tar.gz" | tar -xvz

      - name: "Check cache compatibility"
        run: python scripts/check_cache_compat.py --uv-current ./uv --uv-previous ./uv-aarch64-apple-darwin/uv

  system-test-debian:
    needs: build-binary-linux
    name: "check system | python on debian"
    runs-on: ubuntu-latest
    container: debian:bookworm
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3.11)

      - name: "Validate global Python install"
        run: python3.11 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-fedora:
    needs: build-binary-linux
    name: "check system | python on fedora"
    runs-on: ubuntu-latest
    container: fedora:41
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: dnf install which -y && python3 -m ensurepip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-ubuntu:
    needs: build-binary-linux
    name: "check system | python on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: python scripts/check_system_python.py --uv ./uv

  system-test-opensuse:
    needs: build-binary-linux
    name: "check system | python on opensuse"
    runs-on: ubuntu-latest
    container: opensuse/tumbleweed
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: zypper install -y python310 which && python3.10 -m ensurepip && mv /usr/bin/python3.10 /usr/bin/python3

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  # Note: rockylinux:8 is a 1-1 code compatible distro to rhel-8
  # rockylinux:8 mimics centos-8 but with added maintenance stability
  # and avoids issues with centos stream uptime concerns
  system-test-rocky-linux:
    needs: build-binary-linux
    name: "check system | python on rocky linux"
    runs-on: ubuntu-latest
    container: rockylinux:8
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: |
          dnf install python39 python39-pip which -y

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-pypy:
    needs: build-binary-linux
    name: "check system | pypy on ubuntu"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "pypy3.9"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which pypy)

      - name: "Validate global Python install"
        run: pypy scripts/check_system_python.py --uv ./uv

  system-test-pyston:
    needs: build-binary-linux
    name: "check system | pyston"
    runs-on: ubuntu-latest
    container: pyston/pyston:2.3.5
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which pyston)

      - name: "Validate global Python install"
        run: pyston scripts/check_system_python.py --uv ./uv

  system-test-alpine:
    needs: build-binary-linux
    name: "check system | alpine"
    runs-on: ubuntu-latest
    container: alpine:latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: apk add --update --no-cache python3 py3-pip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-aarch64:
    needs: build-binary-macos-aarch64
    name: "check system | python on macos aarch64"
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      # This should be the macOS system Python
      # We'd like to test with Homebrew but this Python takes precedence in system Python discovery
      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-aarch64-homebrew:
    needs: build-binary-macos-aarch64
    name: "check system | homebrew python on macos aarch64"
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: brew install python3

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-aarch64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv --externally-managed

  system-test-macos-x86_64:
    needs: build-binary-macos-x86_64
    name: "check system | python on macos x86_64"
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      # We test with GitHub's Python as a regression test for
      # https://github.com/astral-sh/uv/issues/2450
      - uses: actions/setup-python@v5

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-macos-x86_64-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv

  system-test-windows-python-310:
    needs: build-binary-windows
    name: "check system | python3.10 on windows"
    runs-on: windows-latest
    env:
      # Avoid debug build stack overflows.
      UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: py -3.10 ./scripts/check_system_python.py --uv ./uv.exe

  system-test-windows-x86-python-310:
    needs: build-binary-windows
    name: "check system | python3.10 on windows x86"
    runs-on: windows-latest
    env:
      # Avoid debug build stack overflows.
      UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.10"
          architecture: "x86"

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: python ./scripts/check_system_python.py --uv ./uv.exe

  system-test-windows-python-313:
    needs: build-binary-windows
    name: "check system | python3.13 on windows"
    runs-on: windows-latest
    env:
      # Avoid debug build stack overflows.
      UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
          allow-prereleases: true
          cache: pip

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python)

      - name: "Validate global Python install"
        run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe

  system-test-choco:
    needs: build-binary-windows
    name: "check system | python3.12 via chocolatey"
    runs-on: windows-latest
    env:
      # Avoid debug build stack overflows.
      UV_STACK_SIZE: 2000000 # 2 megabyte, double the default on windows
    steps:
      - uses: actions/checkout@v4

      - name: "Install Python"
        run: choco install python3 --verbose --version=3.9.13

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-windows-${{ github.sha }}

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: py -3.9 ./scripts/check_system_python.py --uv ./uv.exe

  system-test-pyenv:
    needs: build-binary-linux
    name: "check system | python via pyenv"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: "Install pyenv"
        uses: "gabrielfalcao/pyenv-action@v18"
        with:
          default: 3.9.7

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3.9)

      - name: "Validate global Python install"
        run: python3.9 scripts/check_system_python.py --uv ./uv

  system-test-conda:
    needs:
      [build-binary-windows, build-binary-macos-aarch64, build-binary-linux]
    name: check system | conda${{ matrix.python-version }} on ${{ matrix.os }}
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        os: ["linux", "windows", "macos"]
        python-version: ["3.8", "3.11"]
        include:
          - { os: "linux", target: "linux", runner: "ubuntu-latest" }
          - { os: "windows", target: "windows", runner: "windows-latest" }
          - { os: "macos", target: "macos-aarch64", runner: "macos-14" }
    steps:
      - uses: actions/checkout@v4

      - uses: conda-incubator/setup-miniconda@v3
        with:
          miniconda-version: "latest"
          activate-environment: uv
          python-version: ${{ matrix.python-version }}

      - name: Conda info
        shell: bash -el {0}
        run: conda info

      - name: Conda list
        shell: pwsh
        run: conda list

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-${{ matrix.target }}-${{ github.sha }}

      - name: "Prepare binary"
        if: ${{ matrix.os != 'windows' }}
        run: chmod +x ./uv

      - name: "Print Python path"
        shell: bash -el {0}
        run: echo $(which python)

      - name: "Validate global Python install"
        shell: bash -el {0}
        run: python ./scripts/check_system_python.py --uv ./uv

  system-test-amazonlinux:
    needs: build-binary-linux
    name: "check system | amazonlinux"
    runs-on: ubuntu-latest
    container: amazonlinux:2023
    steps:
      - name: "Install base requirements"
        run: |
          # Needed for `actions/checkout`
          yum install tar gzip which -y
      - uses: actions/checkout@v4
      - name: "Install Python"
        run: yum install python3 python3-pip -y

      - name: "Download binary"
        uses: actions/download-artifact@v4
        with:
          name: uv-linux-${{ github.sha }}

      - name: "Prepare binary"
        run: chmod +x ./uv

      - name: "Print Python path"
        run: echo $(which python3)

      - name: "Validate global Python install"
        run: python3 scripts/check_system_python.py --uv ./uv
